#!/bin/bash
# .
# . 脚本说明 :
# . 脚本功能就是启动, 停止, 重启 curl_conversion 项目, 满足功能的前提下, 脚本尽可能做到了通用性 ...
# . 通用性方面, 脚本通过两方面的设计提高了通用性
# .     1. 拆分函数
# .     2. 提供参数
# .
# .
# . 脚本帮助 :
# . "./exec.sh -h"
# . "./exec.sh --help"
# . "./exec.sh help"
# .
# .
# . LF / CR LF 问题 :
# .     Windows 的模式和 Linux 模式不同, Windows 采用 CR LF, Linux 采用 LF
# .     1. 在 Pycharm 中编写 sh 脚本时, 就采用 LF 模式
# .     2. 通过 git 来控制, push 时更新 sh 到 LF 模式
# .
# .
# . 执行权限问题 :
# .     首次将 sh 脚本推送到 Linux 服务器时, 是没有执行权限的, 需要执行 chmod +x 命令赋予执行权限
# .     赋予执行权限后, Linux 下相当于编辑过该 sh 脚本文件了, 需要推送一次, Windows 下拉取一次即可
# .


# 是否展示启动动画 : true[展示]/false[不展示], 当脚本用于 CI/CD 时, 请配置为 false ...
SHOW_ANIMATION=true


# 默认配置 ( 均可通过参数修改 )
# 端口 :
PORT=5555
# 工作进程数 :
WORKERS=3
# 启动的 flask 路径 :
APP_PATH="run:app"
# 日志文件 :
LOG_FILE="gunicorn.log"
# 日志 :
OUTPUT_FILE="gunicorn.out"


# 解析长参数和短参数 ( getopt 规定短参, --long 规定长参 )
PARSED_ARGS=$(getopt -o p:w:a:l:o:h --long port:,workers:,app-path:,log-file:,output-file:,help -- "$@")
eval set -- "$PARSED_ARGS"


# 显示帮助信息 :
show_help() {
    cat << EOF
Usage : ./exec.sh <command> [options]

Commands :
  start    启动 Gunicorn 实例 ( PORT, WORKERS, APP_PATH, LOG_FILE, OUTPUT_FILE )
  stop     停止 Gunicorn 实例 ( PORT )
  restart  重启 Gunicorn 实例 ( PORT, WORKERS, APP_PATH, LOG_FILE, OUTPUT_FILE )
  pid      显示指定端口号进程的 PID ( PORT )
  help     显示脚本使用帮助信息

Options :
  -p[--port]         PORT          指定端口, 默认值: 5555
  -w[--workers]      WORKERS       指定工作进程数, 默认值: 3
  -a[--app-path]     APP_PATH      指定启动路径, 默认值: "run:app"
  -l[--log-file]     LOG_FILE      指定日志文件, 默认值: "gunicorn.log"
  -o[--output-file]  OUTPUT_FILE   指定输出文件, 默认值: "gunicorn.out"
  -h[--help]                       显示帮助信息

Examples :
  1. 短参 ::
  ./exec.sh start -p 5555 -w 3 -a "run:app" -l "gunicorn.log" -o "gunicorn.out"
  2. 长参 ::
  ./exec.sh start --port 5555 --workers 3 --app-path "run:app" --log-file "gunicorn.log" --output-file "gunicorn.out"
  3. 无参 ::
  ./exec.sh restart
  ./exec.sh help
EOF
}


# 解析参数, 短参和长参 :
while true; do
    case "$1" in
        -p | --port) PORT="$2"; shift 2 ;;
        -w | --workers) WORKERS="$2"; shift 2 ;;
        -a | --app-path) APP_PATH="$2"; shift 2 ;;
        -l | --log-file) LOG_FILE="$2"; shift 2 ;;
        -o | --output-file) OUTPUT_FILE="$2"; shift 2 ;;
        -h | --help) show_help; exit 0 ;;
        --) shift; break ;;
        *) show_help; exit 1 ;;
    esac
done


# 启动动画 ( $SHOW_ANIMATION 控制 ) :
start_cartoon() {
    if [ "$SHOW_ANIMATION" = true ]; then
        echo -n "开始启动项目"
        animation=( '-' '\' '|' '/' '-')
        dots=3
        interval=0.5
        current_output=""
        for (( i=0; i<$dots; i++ )); do
            for anim_char in "${animation[@]}"; do
                printf "\r开始启动项目 ${current_output}$anim_char"
                sleep $interval
            done
            current_output+="."
            printf "\r开始启动项目 ${current_output}"
        done
        echo " ... ... 启动完成 ..."
    fi
}


# 检查端口是否被占用 :
is_port_in_use() {
    # echo "检查端口 <$1> 是否被占用 ..."
    lsof -i :$1 > /dev/null 2>&1
    if [ $? -eq 0 ]; then
        echo "启动前进行端口检查, 端口 5555 正在被使用 ..."
        return 0
    else
        echo "启动前进行端口检查, 端口 5555 未被使用 ..."
        return 1
    fi
}


# 依赖简单检查检查 :
source_checks() {
    # 检查 Python3 是否已安装 :
    # echo "依赖项 Python3 检查 :"
    if ! command -v python3 &> /dev/null; then
        echo "Python3 未安装, 请先安装并正确配置 Python3 ."
        exit 1
    fi
    # echo "依赖项 Python3 检查通过 ."

    # 检查 Gunicorn 是否已安装 :
    # echo "依赖项 Gunicorn 检查 :"
    if ! python3 -c "import gunicorn" &> /dev/null; then
        echo "Gunicorn 未安装, 请先安装并正确配置 Gunicorn ."
        exit 1
    fi
    # echo "依赖项 Gunicorn 检查通过 ."

    # 检查端口是否可用 :
    if is_port_in_use $PORT; then
        echo "Gunicorn 可能正在运行 ."
        exit 1
    fi

    echo "依赖项检查通过"

    # 检查是否包含 requirements.txt 文件 :
    if [ -f "requirements.txt" ]; then
        # 自动安装三方库 : ( 生产化境不推荐, 可能有风险 )
        # pip3 install -r requirements.txt || { echo "安装 Python 三方库依赖失败"; exit 1; }
        # echo "所有 Python 三方库依赖已成功安装 ."
        echo "如首次运行, 可参考命令 {{ pip3 install -r requirements.txt }} 安装三方库资源"
    else
        echo "警告: 没有找到 requirements.txt 文件, 如首次运行请确保 Python 三方依赖库均正确安装"
    fi
}


# 获取占用端口的进程 ID :
get_pids_by_port() {
    sudo lsof -i :$1 -sTCP:LISTEN -t
}


# 后台启动 Gunicorn :
start_gunicorn() {
    # echo "启动 Gunicorn 实例 ..."
    nohup python3 -m gunicorn -w $WORKERS -b 0.0.0.0:$PORT $APP_PATH --log-file=$LOG_FILE &> $OUTPUT_FILE &
    if [ $? -ne 0 ]; then
        echo "启动失败, 请检查配置或日志文件 ."
        exit 1
    fi
    # 给一个假的启动动画 :
    start_cartoon;
    # 打印 URL :
    IP=$(hostname -I | awk '{print $1}')
    URL="http://${IP}:${PORT}"
    printf "请访问 %s 查看页面\n" "$URL"
    # printf 比 echo 稳定些 !
    # echo "请访问 ${URL} 查看页面"
    # echo -e "请访问 ${URL} 查看页面"
    echo "如果访问失败, 请依次执行停止命令和启动命令后, 重新访问 ..."
}


# 停止 Gunicorn :
stop_gunicorn() {
    pids=$(get_pids_by_port $PORT)
    if [ -z "$pids" ]; then
        echo "没有找到运行在端口 $PORT 上的进程 ."
    else
        echo "找到运行中的进程, PID : "
        # 打印下 pid :
        for pid in $pids; do
            echo "- - - - - -> {{ $pid }} "
        done
        # 软的不行就来硬的 :
        for pid in $pids; do
            # 软的 :
            echo "尝试终止进程: $pid ..."
            if ps -p $pid > /dev/null; then
                kill -15 $pid
            fi
            # 等待 1 秒给系统处理进程资源 :
            sleep 1
            # 硬的 :
            if ps -p $pid > /dev/null; then
                echo "进程 $pid 没有停止, 尝试强制终止 ..."
                kill -9 $pid
            else
                echo "进程 $pid 已成功停止 ."
            fi
        done
        # 等待 1 秒确保端口已释放
        sleep 1
        # 使用 pkill 进一步确保所有相关进程被终止
        if pkill -f "gunicorn.*:$PORT"; then
            echo "进一步杀死 $PORT 端口进程, 全部进程均已成功停止 ."
        else
            echo "进一步检查 $PORT 端口进程, 全部进程均已成功停止 ."
        fi
    fi
}


# 等待指定端口被释放 :
wait_for_port_release() {
    local port=$1
    local max_wait=${2:-15}
    local waited=0
    local port_released=false

    while [ $waited -lt $max_wait ]; do
        if ! is_port_in_use "$port"; then
            port_released=true
            break
        fi
        echo "等待端口 $port 资源释放 ... (已等待 $waited 秒)"
        sleep 1
        ((waited++))
    done

    if [ "$port_released" = true ]; then
        return 0
    else
        return 1
    fi
}


# 提取参数, 执行逻辑 :
case "$1" in
    pid)
        # 输出 $PORT 端口的所有 pid :
        get_pids_by_port $PORT
        ;;
    start)
        # 简单检查依赖 :
        source_checks
        # 启动 :
        start_gunicorn
        ;;
    stop)
        # 停止 :
        stop_gunicorn
        ;;
    restart)
        # 停止 :
        stop_gunicorn
        # 等待端口释放 :
        if wait_for_port_release "$PORT"; then
          # 启动 :
          start_gunicorn
        else
            echo "警告: 端口 $PORT 在规定时间内未能释放, 请手动检查后再重启 ."
        fi
        ;;
    help|--help|-h)
        show_help
        ;;
    *)
        if [ -z "$1" ]; then
            echo "请参考提示添加参数进行调用"
        else
            echo "未知的参数: $1"
        fi
        echo "有效的参数有: start / stop / restart / pid"
        echo "或者使用 -h / --help / help 查看帮助"
        exit 1
        ;;
esac

exit 0
